<?php namespace sacvoyage\storage;

use sacvoyage\Object;
use sacvoyage\exception\InputException;
use sacvoyage\exception\FilterException;
use sacvoyage\exception\InternalException;

/**
 * Iterator-like object, that retreives array('_id'=>$_id, '_rev': $_rev)-pairs
 * from database according to filter
 *
 * You really shouldn't use this, when you are selecting by id
 */
class Filter implements \Iterator {


	/**
	 * Construct a filter
	 *
	 * @param	Gateway	gateway	
	 * @param	Table	table to work on
	 * @param	boolean	includeHistory whether output historical versions
 	 *                  of documents or not
	 * @param	array	filter
	 *
	 * 
	 *
	 */
	public function __construct(&$table, $jsonFilter=null) {
		$this->_table=&$table;

		if($jsonFilter) {
			$this->_keys=$this->_filter(array(), $jsonFilter);
		} else {
			$this->_keys=$this->_select();
		}
	}

	public function rewind() {
		return reset($this->_keys);
	}

	public function key() {
		return key($this->_keys);
	}

	public function current() {
		return current($this->_keys);
	}
	
	public function next() {
		return next($this->_keys);
	}

	public function valid() {
		return current($this->_keys)!==false;
	}

	private function _getLastVersions() {
		return $this->_select(
			"_rev=(SELECT MAX(maxtable._rev) ".
			"FROM {$this->_table} AS maxtable ".
			"WHERE {$this->_table}._id=maxtable._id)"
		);
	}


	/*
	 * make ".books[1]['Alice in Wonderland'].isbn"-like string 
	 * from array('books', 1, 'Alice in Wonderland', 'isbn')-like array
	 *
	 */
	private function _pathStr($stack) {
		if (is_enumerative_array($stack)) {
			$path="";
			foreach($stack as $key) {
				if(is_integer($key)) {
					$path.="[$key]";
				} elseif (is_string($key)) {
					if(preg_match('/^[_A-Za-z][_A-Za-z0-9]*$/', $key)) {
		          $path.=".$key";
		      } else {
		          $escapedKey = array_replace("'", "\\'", $key);
		          $path.="['$escapedKey']";
		      }
				} else {
					throw new InputException("stack elements must be integers or strings to build path");
				}
			}
			return $path;
		} else {
        throw new InputException('stack must be enumerative array to build path');
		}
	}

	/*
	 * get type name of given value according to json-specification
	 */
	private function _getTypeStr($value) {
		if    (is_string($value))  { return 'string'; }
		elseif(is_numeric($value)) { return 'number'; }
		elseif(is_object($value))  { return 'object'; }
		elseif(is_array($value))   { return 'array'; }
		elseif(is_boolean($value)) { return 'boolean'; }
		elseif(is_null($value))    { return 'null'; }
		else {
			throw new InternalException('wrong json-element type');
		}
	}
	
	/*
	 * assert that $type is valid type name according to json-specification
	 */
	private function _assertType($type, $errMsg=null) {
		if(!in_array($type, array('string', 'number', 'object', 
		                          'array', 'boolean', 'null'))) {
			if(!$errMsg) {
				$errMsg=$type.
				' is invalid type-name; must be one of:'.
				' string, number, object, array, boolean, null';
			}
			throw new InternalException($errMsg);
		};
	}

	private function _select($where='') {
		$sql="SELECT DISTINCT _id, _rev ".
		     "FROM {$this->_table->getExpression()} ".
		     "AS {$this->_table->getAlias()}";
		if($where) {
			$sql.=" WHERE $where";
		}
		$result=$this->_table->query($sql);
		$array=array();
		while($row=$result->fetch_assoc()) {
			$array[]=new Object($row);
		}
		$result->free();
		return $array;
	}

	private function _not($stack, $filter) {
		return array_of_objects_diff(
			$this->_select(), 
			$this->_filter($stack, $filter)
		);
	}

	private function _and($stack, $filters) {
		if(!is_enumerative_array($filters)) {
			throw new InputException('_and-argument must be array');
		}
		$sets=array();
		foreach($filters as $filter) {
			$sets[]=$this->_filter($stack, $filter);
		}
		return call_user_func_array('array_of_objects_intersect', $sets);
	}

	private function _or($stack, $filters) {
		if(!is_enumerative_array($filters)) {
			throw new InputException('_or-argument must be array');
		}
		$result=array();
		foreach($filters as $filter) {
			foreach($this->_filter($stack, $filter) as $object) {
				if(!in_array($object, $result)) {
					$result[]=$object;		// TODO: optimize			
				}
			}
		}
		return $result;
	}

	private function _exists($stack, $value) {
		// we need not check if $value is boolean,
		// any value, resulting in true, assumed to be true		
		$path=$this->_table->escape($this->_pathStr($stack));
		if($value) {
			return $this->_select("path='$path'");
		} else {
			return $this->_select(
				"NOT '$path' IN (SELECT path ".
				"FROM {$this->_table->getExpression()} AS pathtable ".
				"WHERE pathtable._id={$this->_table->getAlias()}._id AND ".
				"pathtable._rev={$this->_table->getAlias()}._rev)"
			);
		}
	}

	private function _is($stack, $type) {
		$this->_assertType(
			$type, 
			'"_is"-argument must be type-name, given '.$type
		);
		$path=$this->_table->escape($this->_pathStr($stack));
		return $this->_select("path='$path' AND type='$type'");
	}

	private function _nis($stack, $type) {
		$this->_assertType(
			$type, 
			'"_is"-argument must be type-name, given '.$type
		);
		$path=$this->_table->escape($this->_pathStr($stack));
		return $this->_select("path='$path' AND type<>'$type'");
	}

	private function _eq($stack, $value) {
		if(!is_string($value) 
		&& !is_numeric($value) 
		&& !is_bool($value) 
		&& !is_null($value)) {
			throw new InputException('"_eq"-argument cannot be '.gettype($value));
		}
		$path=$this->_table->escape($this->_pathStr($stack));
		$value=$this->_table->escape(json_to_string($value));
		return $this->_select("path='$path' AND value='{$value}'");
	}

	private function _in($stack, $alternatives) {	
		$path=$this->_table->escape($this->_pathStr($stack));
		$alts=array_map(array($this->_table, 'escape'), $alternatives);
		return $this->_select("path='$path' AND value IN ('".implode("', '", $alts)."')");
	}

	private function _ne($stack, $value) {
		if(!is_string($value) 
		&& !is_numeric($value) 
		&& !is_bool($value) 
		&& !is_null($value)) {
			throw new InputException('"_eq"-argument cannot be '.gettype($value));
		}
		$path=$this->_table->escape($this->_pathStr($stack));
		$value=$this->_table->escape(json_to_string($value));
		return $this->_select(
			"(SELECT path ".
			"FROM {$this->_table->getExpression()} AS lookuptable ".
			"WHERE lookuptable._id={$this->_table->getAlias()}._id AND ".
			"lookuptable._rev={$this->_table->getAlias()}._rev ".
			"AND lookuptable.path='$path' AND lookuptable.value='$value'".
			") IS NULL"
		);
	}

	private function _nin($stack, $alternatives) {
		$path=$this->_table->escape($this->_pathStr($stack));
		$alts=array_map(array($this->_table, 'escape'), $alternatives);
		if($alts) {
			return $this->_select(
				"(SELECT path ".
				"FROM {$this->_table->getExpression()} AS lookuptable ".
				"WHERE lookuptable._id={$this->_table->getAlias()}._id AND ".
				"lookuptable._rev={$this->_table->getAlias()}._rev ".
				"AND lookuptable.path='$path' ".
				"AND lookuptable.value IN ('".implode("', '", $alts)."')".
				") IS NULL"
			);
		} else {
			return $this->_select();
		}
	}

	private function _compare($stack, $operator, $value) {
		$path=$this->_table->escape($this->_pathStr($stack));
		$type=$this->_getTypeStr($value);
		if(is_string($value)) {
			$value=$this->_table->escape(json_to_string($value));
			return $this->_select(
				"path='$path' AND value $operator '$value'"
			);
		} elseif(is_numeric($value)) {
			return $this->_select(
				"path='$path' AND CAST(value AS SIGNED) $operator $value"
			);
		} else {
			throw new InputException(
				'comparison operator argument must be string or number, '.
				'not '.$type
			);
		}
	}

	private function _gt($stack, $value) {	
		return $this->_compare($stack, '>', $value);
	}

	private function _lt($stack, $value) {	
		return $this->_compare($stack, '<', $value);
	}

	private function _ge($stack, $value) {	
		return $this->_compare($stack, '>=', $value);
	}

	private function _le($path, $value) {	
		return $this->_compare($stack, '<=', $value);
	}
	
	private function _regex($stack, $regex) {	
		$path=$this->_table->escape($this->_pathStr($stack));
		$regex=$this->_table->escape(json_to_string($regex));
		return $this->_select("path='$path' AND value REGEXP '$regex'");
	}

	/*
	 * return sql query returning _id's and _rev's of matching documents
	 */
	private function _filter($stack, $filter) {
		if(is_string($filter) 
		|| is_numeric($filter) 
		|| is_bool($filter) 
		|| is_null($filter)) {
			return $this->_eq($stack, $filter); 
		} elseif(is_object($filter) && count((array)$filter)>0) {
			$set=null; // null but not array(), while intersecting 
			foreach($filter as $key=>$value) {
				if(in_array($key, array("_not", "_and", "_or", 
				"_exists", "_is", "_nis", "_eq", "_in", "_ne", "_nin",
				"_gt", "_lt", "_ge", "_le", "_regex"))) {
					$keys=$this->$key($stack, $value);
				} else {
					$pushStack=array_merge($stack, array($key));
					$keys=$this->_filter($pushStack, $value);
				}
				if(is_null($set)) {
					$set=$keys;
				} else {
					$set=array_of_objects_intersect($set, $keys);
				}
			}
			return $set;
		} elseif(is_object($filter)) {
			return $this->_select();
		} elseif(is_associative_array($filter)) {
			throw new InternalException('filter cannot be associative array');
		} else {
			throw new InternalException($filter.' is invalid filter element');
		}
	}



}



?>
